指向函数的指针
对于一个函数只能做两件事:调用它,或者取得它的地址。通过取一个函数的地址而得到的指针,可以在后面用于调用这个函数。例如,
void error(string s) { /* ... */ }
void (*efct)(string); // 指向函数的指针
void f()
{
efct = &error; // efct指向error
efct("error"); // 通过efct调用error
}
编译器知道efct是一个指针,并会去调用被指的函数。这也就是说,可以不写从指针得到函数的间接运算 *。与此类似,取得函数地址的 & 也可以不写:
void (*f1)(string) = &error; // ok
void (*f2)(string) = error; // 也可以:与&error意思一样
void g()
{
f1("Vasa"); // ok
(*f1)("Mary Rose"); // 也可以
}
在指向函数的指针的声明中也需要给出参数类型,就像函数声明一样。在指针赋值时,完整的函数类型必须完全匹配。例如,
void (*pf)(string); // 指向void(string)
void f1(string); // void(string)
int f2(string); // int(string)
void f3(int*); // void(int*)
void f()
{
pf = &f1; // ok
pf = &f2; // 错误❌:返回类型不对
pf = &f3; // 错误❌:参数类型不对
pf("Hera"); // ok
pf(1); // 错误❌:参数类型不对
int i = pf("Zeus"); // 错误❌:void赋值给int
}
无论直接调用函数或通过某个指针去调用函数,有关参数传递的规则都完全相同。
人们常常为了方便而为指向函数的指针类型定义一个名字,这样可以避免到处去写意义不太明显的语法形式。下面是来自UNIX系统头文件的一个例子:
typedef void (*SIG_TYP)(int); // 取自<signal.h>
typedef void (*SIG_ARG_TYP)(int);
SIG_TYP signal(int, SIG_ARG_TYP);
指向函数的指针的数组常常很有用。例如,我的基于鼠标的编辑器里的菜单系统,就是利用指向函数的指针的数组实现的,这些函数表示各种各样的操作。这个系统的细节不可能在这里描述,但下面是其中的基本思想:
typedef void (*PF)();
PF edit_ops[] = { // 编辑操作
&cut, &paste, ©, &search
};
PF file_ops[] = { // 文件管理
&open, &append, &close, &write
};
然后我们就可以定义并初始化一些指针,由它们去控制各种操作,通过关联于鼠标键的菜单去选择那些操作:
PF* button2 = edit_ops;
PF* button3 = file_ops;
在一个完整的实现里,定义一个菜单项需要提供更多的信息。例如,必须在某个地方保存有关需要显示的字符串的一个描述。随着系统的使用,鼠标键的意义也可能随环境而频繁变化,这种变化就可以(部分地)通过修改按键所对应的指针来实现。当用户选择一个菜单项时,例如按键2的项目3,就会执行相关的操作:
button2[2](); // 调用按键2的第3个函数
要理解指向函数的指针的表达能力,一种方式就是试着去写这种代码而不用函数指针---也不用它们的更具良好行为的兄弟:虚函数(12.2.6节)。通过把新函数插入运算符表等方式,就可以在运行中修改这种菜单。在运行中构造出新菜单也同样非常容易。
指向函数的指针可以用于提供一种简单形式的多态性例程,即那种可以应用于许多不同类型的对象的例程:
typedef int (*CFT)(const void*, const void*);
void ssort(void* base, size_t n, size_t sz, CFT cmp)
/*
对向量base的n个元素按照递增顺序排序,
用由"cmp"所指的函数做比较,
元素的大小是"sz"。
Shell排序(Knuth, Vol 3, Pg84)
*/
{
for(int gap = n/2; gap > 0; gap /= 2)
for(int i = gap; i < n; i++)
for(int j = i - gap; j >= 0; j-=gap) {
char* b = static_cast<char*>(base); // 必须强制
char* pj = b + j * sz; // &base[j]
char* pjg = b + (j + gap)*sz; // &base[j+gap]
if(cmp(pjg, pj) < 0) { // 交换base[j]与base[j + gap]:
for(int k = 0; k < sz; k++) {
char temp = pj[k];
pj[k] = pjg[k];
pjg[k] = temp;
}
}
}
}
ssort()例程并不知道被排序的对象的类型,它只知道元素的个数(数组大小),每个元素的大小,以及应该去调用以完成比较的函数。这里有意将ssort()的类型选的与C标准库排序例程qsort()完全一样。实际程序可以使用qsort(),C++标准库算法sort(18.7.1节),或者其他特殊的排序例程。这种风格的代码在C里很常见,但它不是在C++里表述这个算法的最优美的方式(13.3节、13.5.2节)。
这个排序函数可用于对下面这样的表格的排序:
struct User {
char* name;
char* id;
int dept;
};
User heads[] = {
"Ritchie D.M.", "dmr", 11271,
"Sethi R.", "ravi", 11272,
"Szymanski T.G.", "tgs", 11273,
"Schryer N.L.", "nls", 11274,
"Schryer N.L.", "nls", 11275,
"Kernighan B.W.", "bwk", 11276
};
void print_id(User* v, int n)
{
for(int i = 0; i < n; i++)
cout << v[i].name << '\t' << v[i].id << '\t' << v[i].dept << '\n';
}
为能完成排序,我们首先需要定义一个适当的比较函数。这种比较函数应该在其第一个参数小于第二个参数时返回负值,如果它们相等就返回0,否则就返回正值:
int cmp1(const void* p, const void* q) // 比较名字串
{
return strcmp(static_cast<const User*>(p)->name, static_cast<const User*>(q)->name);
}
int cmp2(const void* p, const void* q) // 比较部门编号
{
return static_cast<const User*>(p)->dept - static_cast<const User*>(q)->dept;
}
下面程序完成排序和打印:
int main()
{
cout << "Heads in alphabetical order:\n";
ssort(heads, 6, sizeof(User), cmpl);
print_id(heads, 6);
cout << '\n';
cout << "Heads in order of department number:\n";
ssort(heads, 6, sizeof(User), cmp2);
print_id(heads, 6);
}
你可以通过赋值或者初始化指向函数的指针的方式,取得一个重载函数的地址。在这种情况下,从一组重载函数中选择的工作是通过目标指针的类型实现的。例如,
void f(int);
int f(char);
void (*pf1)(int) = &f; // void f(int)
int (*pf2)(char) = &f; // int f(char)
void (*pf3)(char) = &f; // 错误❌:没有void f(char)
要通过指向函数的指针调用的函数,其参数类型和返回值类型都必须与指针的要求完全一致,在用函数对指针赋值或初始化时,没有隐含的参数或者返回值类型转换。这意味着
int cmp3(const mytype*, const mytype*);
不是ssort()的合适参数。究其原因,接受cmp3作为ssort的参数将会违反有关的保证:cmp3调用时参数的类型是mytype*(9.2.5节)。
🔚